{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b94295e3-3bbc-478f-a6eb-a5550e524573",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "pn.extension()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34c90e0e-c932-4346-8452-72f9c3aeecbb",
   "metadata": {},
   "source": [
    "`JSComponent` simplifies the creation of custom Panel components using JavaScript."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f7e59ce8-e218-41b9-bb9c-4c5685ed5b44",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "import param\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "\n",
    "class CounterButton(JSComponent):\n",
    "\n",
    "    value = param.Integer()\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      let btn = document.createElement(\"button\");\n",
    "      btn.innerHTML = `count is ${model.value}`;\n",
    "      btn.addEventListener(\"click\", () => {\n",
    "        model.value += 1\n",
    "      });\n",
    "      model.on('value', () => {\n",
    "        btn.innerHTML = `count is ${model.value}`;\n",
    "      })\n",
    "      return btn\n",
    "    }\n",
    "    \"\"\"\n",
    "\n",
    "CounterButton().servable()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "188e2bf7-1bf0-4a61-a867-7770dd54eb46",
   "metadata": {},
   "source": [
    ":::{note}\n",
    "`JSComponent` was introduced in June 2024 as a successor to `ReactiveHTML`.\n",
    "\n",
    "`JSComponent` bears similarities to [`AnyWidget`](https://anywidget.dev/), but it is specifically optimized for use with Panel.\n",
    "\n",
    "If you are looking to create custom components using Python and Panel component only, check out [`Viewer`](Viewer.md).\n",
    ":::\n",
    "\n",
    "## API\n",
    "\n",
    "### JSComponent Attributes\n",
    "\n",
    "- **`_esm`** (str | PurePath): This attribute accepts either a string or a path that points to an  [ECMAScript module](https://nodejs.org/api/esm.html#modules-ecmascript-modules). The ECMAScript module should export a `render` function which returns the HTML element to display. In a development environment such as a notebook or when using `--dev` flag, the module will automatically reload upon saving changes.\n",
    "- **`_importmap`** (dict | None): This optional dictionary defines an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap), allowing you to customize how module specifiers are resolved.\n",
    "- **`_render_policy`** (Literal['manual', 'children']): Policy that determines when to to re-render (i.e. call the render function) the component. By default `render_policy='children'`, which triggers a full re-render when a child is updated. In `'manual'` mode updates to children have to be manually handled using `.on` callbacks.  \n",
    "- **`_stylesheets`** (optional list of strings): This optional attribute accepts a list of CSS strings or paths to CSS files. It supports automatic reloading in development environments.\n",
    "\n",
    ":::note\n",
    "\n",
    "You may specify a path to a file as a string instead of a PurePath. The path should be specified relative to the file its specified in.\n",
    "\n",
    ":::\n",
    "\n",
    "### `render` Function\n",
    "\n",
    "The `_esm` attribute must export the `render` function. It accepts the following parameters:\n",
    "\n",
    "- **`model`**: Represents the Parameters of the component and provides methods to add (and remove) event listeners using `.on` and `.off`, render child elements using `.get_child`, and to `.send_event` back to Python.\n",
    "- **`view`**: The Bokeh view.\n",
    "- **`el`**: The HTML element that the component will be rendered into.\n",
    "\n",
    "Any HTML element returned from the `render` function will be appended to the HTML element (`el`) of the component but you may also manually append to and manipulate the `el` directly.\n",
    "\n",
    "### Callbacks\n",
    "\n",
    "The `model.on` and `model.off` methods allow registering event handlers inside the render function. This includes the ability to listen to parameter changes and register lifecycle hooks.\n",
    "\n",
    "#### Change Events\n",
    "\n",
    "The following signatures are valid when listening to change events:\n",
    "\n",
    "- `.on('<parameter>', callback)`: Allows registering an event handler for a single parameter.\n",
    "- `.on(['<parameter>', ...], callback)`: Allows adding an event handler for multiple parameters at once.\n",
    "- `.on('change:<parameter>', callback)`: The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap.\n",
    "- \n",
    "\n",
    "The `change:` prefix allows disambiguating change events from lifecycle hooks should a parameter name and lifecycle hook overlap.\n",
    "\n",
    "#### Bidirectional Events\n",
    "\n",
    "##### JS -> Python\n",
    "\n",
    "- `.send_event('<name>', DOMEvent)`: Allows sending browser `DOMEvent` to Python and associating it with a name. An event handler can be registered by name with the `.on_event` method or by implementing a `_handle_<name>` method on the class.\n",
    "- `.send_msg(data)`: Allows sending arbitrary data to Python. An event handler can be registered with the `.on_msg(callback)` method on the Python component or by implementing a `_handle_msg` method on the class.\n",
    "\n",
    "##### Python -> JS\n",
    "\n",
    "- `._send_event(ESMEvent, data=msg)`: Allows sending arbitrary data to the frontend, which can be observed by registering a handler with `.on('msg:custom', callback)`.\n",
    "\n",
    "\n",
    "#### Lifecycle Hooks\n",
    "\n",
    "- `.on('after_layout', callback)`: Called whenever the layout around the component is changed.\n",
    "- `.on('after_render', callback)`: Called once after the component has been fully rendered.\n",
    "- `.on('resize', callback)`: Called after the component has been resized.\n",
    "- `.on('remove', callback)`: Called when the component view is being removed from the DOM.\n",
    "\n",
    "The `lifecycle:` prefix allows disambiguating lifecycle hooks from change events should a parameter name and lifecycle hook overlap.\n",
    "\n",
    "## Usage\n",
    "\n",
    "### Styling with CSS\n",
    "\n",
    "Include CSS within the `_stylesheets` attribute to style the component. The CSS is injected directly into the component's HTML."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0f0ca9f3-547e-4378-8cc8-f745b987b121",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "import param\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "\n",
    "class StyledCounterButton(JSComponent):\n",
    "\n",
    "    value = param.Integer()\n",
    "\n",
    "    _stylesheets = [\n",
    "        \"\"\"\n",
    "        button {\n",
    "            background: #0072B5;\n",
    "            color: white;\n",
    "            border: none;\n",
    "            padding: 10px;\n",
    "            border-radius: 4px;\n",
    "        }\n",
    "        button:hover {\n",
    "            background: #4099da;\n",
    "        }\n",
    "        \"\"\"\n",
    "    ]\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const btn = document.createElement(\"button\");\n",
    "      btn.innerHTML = `count is ${model.value}`;\n",
    "      btn.addEventListener(\"click\", () => {\n",
    "        model.value += 1\n",
    "      });\n",
    "      model.on('value', () => {\n",
    "        btn.innerHTML = `count is ${model.value}`;\n",
    "      })\n",
    "      return btn\n",
    "    }\n",
    "    \"\"\"\n",
    "\n",
    "StyledCounterButton().servable()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d16400f-d238-48c6-aeca-73d65ed62027",
   "metadata": {},
   "source": [
    "## Send Events from JavaScript to Python\n",
    "\n",
    "Events from JavaScript can be sent to Python using the `model.send_event` method. Define a *handler* in Python to manage these events. A *handler* is a method on the form `_handle_<name-of-event>(self, event)`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a21ae67f-2077-4bf8-9184-b02bf84e4326",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "import param\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "\n",
    "class EventExample(JSComponent):\n",
    "\n",
    "    value = param.Parameter()\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const btn = document.createElement('button')\n",
    "      btn.innerHTML = `Click Me`\n",
    "      btn.onclick = (event) => model.send_event('click', event)\n",
    "      return btn\n",
    "    }\n",
    "    \"\"\"\n",
    "\n",
    "    def _handle_click(self, event):\n",
    "        self.value = event.data\n",
    "\n",
    "button = EventExample()\n",
    "\n",
    "event_json = pn.pane.JSON(button.param.value)\n",
    "\n",
    "pn.Column(button, event_json)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4979e5b0-7caa-4ffd-a8bd-40680e048935",
   "metadata": {},
   "source": [
    "You can also define and send arbitrary data using the `.send_msg()` API and by implementing a `_handle_msg` method on the component:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f99cc078-3b4f-43d8-85aa-983263f1a800",
   "metadata": {},
   "outputs": [],
   "source": [
    "import datetime\n",
    "\n",
    "import panel as pn\n",
    "import param\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "\n",
    "class CustomEventExample(JSComponent):\n",
    "\n",
    "    value = param.String()\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const btn = document.createElement('button')\n",
    "      btn.innerHTML = `Click Me`;\n",
    "      btn.onclick = (event) => {\n",
    "        const currentDate = new Date();\n",
    "        model.send_msg(currentDate.getTime())\n",
    "      }\n",
    "      return btn\n",
    "    }\n",
    "    \"\"\"\n",
    "\n",
    "    def _handle_msg(self, msg):\n",
    "        unix_timestamp = msg/1000\n",
    "        python_datetime = datetime.datetime.fromtimestamp(unix_timestamp)\n",
    "        self.value = str(python_datetime)\n",
    "\n",
    "button = CustomEventExample()\n",
    "\n",
    "pn.Column(button, button.param.value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d4b72890-3512-42ad-9352-30c89b7e9c52",
   "metadata": {},
   "source": [
    "## Dependency Imports\n",
    "\n",
    "JavaScript dependencies can be directly imported via URLs, such as those from [`esm.sh`](https://esm.sh/)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a617ff6-a168-4bc2-8348-304e3d85c70d",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "\n",
    "class ConfettiButton(JSComponent):\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    import confetti from \"https://esm.sh/canvas-confetti@1.6.0\";\n",
    "\n",
    "    export function render() {\n",
    "      let btn = document.createElement(\"button\");\n",
    "      btn.innerHTML = \"Click Me\";\n",
    "      btn.addEventListener(\"click\", () => {\n",
    "        confetti()\n",
    "      });\n",
    "      return btn\n",
    "    }\n",
    "    \"\"\"\n",
    "\n",
    "ConfettiButton()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8dd08bad-a752-4102-ac7d-a09c4b96991d",
   "metadata": {},
   "source": [
    "Use the `_importmap` attribute for more concise module references."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "936b653c-637e-4ecd-bc57-324a007c8802",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "\n",
    "class ConfettiButton(JSComponent):\n",
    "    _importmap = {\n",
    "        \"imports\": {\n",
    "            \"canvas-confetti\": \"https://esm.sh/canvas-confetti@1.6.0\",\n",
    "        }\n",
    "    }\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    import confetti from \"canvas-confetti\";\n",
    "\n",
    "    export function render() {\n",
    "      let btn = document.createElement(\"button\");\n",
    "      btn.innerHTML = `Click Me`;\n",
    "      btn.addEventListener(\"click\", () => {\n",
    "        confetti()\n",
    "      });\n",
    "      return btn\n",
    "    }\n",
    "    \"\"\"\n",
    "\n",
    "ConfettiButton()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80a29850-659f-4254-ac94-0a2b78e9e541",
   "metadata": {},
   "source": [
    "See [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) for more info about the import map format."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c890e2e9-9027-4a52-9d21-65fd135b8c38",
   "metadata": {},
   "source": [
    "## External Files\n",
    "\n",
    "You can load JavaScript and CSS from files by providing the paths to these files.\n",
    "\n",
    "Create the file **counter_button.py**.\n",
    "\n",
    "```python\n",
    "from pathlib import Path\n",
    "\n",
    "import param\n",
    "import panel as pn\n",
    "\n",
    "from panel.custom import JSComponent\n",
    "\n",
    "pn.extension()\n",
    "\n",
    "class CounterButton(JSComponent):\n",
    "\n",
    "    value = param.Integer()\n",
    "\n",
    "    _esm = Path(\"counter_button.js\")\n",
    "    _stylesheets = [Path(\"counter_button.css\")]\n",
    "\n",
    "CounterButton().servable()\n",
    "```\n",
    "\n",
    "Now create the file **counter_button.js**.\n",
    "\n",
    "```javascript\n",
    "export function render({ model }) {\n",
    "  let btn = document.createElement(\"button\");\n",
    "  btn.innerHTML = `count is ${model.value}`;\n",
    "  btn.addEventListener(\"click\", () => {\n",
    "    model.value += 1;\n",
    "  });\n",
    "  model.on('value', () => {\n",
    "    btn.innerHTML = `count is ${model.value}`;\n",
    "  });\n",
    "  return btn;\n",
    "}\n",
    "```\n",
    "\n",
    "Now create the file **counter_button.css**.\n",
    "\n",
    "```css\n",
    "button {\n",
    "    background: #0072B5;\n",
    "    color: white;\n",
    "    border: none;\n",
    "    padding: 10px;\n",
    "    border-radius: 4px;\n",
    "}\n",
    "button:hover {\n",
    "    background: #4099da;\n",
    "}\n",
    "```\n",
    "\n",
    "Serve the app with `panel serve counter_button.py --dev`.\n",
    "\n",
    "You can now edit the JavaScript or CSS file, and the changes will be automatically reloaded.\n",
    "\n",
    "- Try changing the `innerHTML` from `count is ${model.value}` to `COUNT IS ${model.value}` and observe the update. Note you must update `innerHTML` in two places.\n",
    "- Try changing the background color from `#0072B5` to `#008080`.\n",
    "\n",
    "## Displaying A Single Child\n",
    "\n",
    "You can display Panel components (`Viewable`s) by defining a `Child` parameter.\n",
    "\n",
    "Lets start with the simplest example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6810c585-c55a-428b-85ac-ca1363dfd33a",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "from panel.custom import Child, JSComponent\n",
    "\n",
    "class Example(JSComponent):\n",
    "\n",
    "    child = Child()\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const button = document.createElement(\"button\");\n",
    "      button.append(model.get_child(\"child\"))\n",
    "      return button\n",
    "    }\"\"\"\n",
    "\n",
    "Example(child=pn.panel(\"A **Markdown** pane!\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e8f2695f-2ced-427c-88d7-414e97e0ec60",
   "metadata": {},
   "source": [
    "If you provide a non-`Viewable` child it will automatically be converted to a `Viewable` by `pn.panel`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d796b164-fe14-44da-8795-666e3d4117a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "Example(child=\"A **Markdown** pane!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7bf894f-579a-4e19-b7e9-011879db5fc4",
   "metadata": {},
   "source": [
    "If you want to allow a certain type of Panel components only you can specify the specific type in the `class_` argument."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "853faef4-c890-4c81-b661-bdba7e89e9df",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "from panel.custom import Child, JSComponent\n",
    "\n",
    "class ChildExample(JSComponent):\n",
    "\n",
    "    child = Child(class_=pn.pane.Markdown)\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const button = document.createElement(\"button\");\n",
    "      button.append(model.get_child(\"child\"))\n",
    "      return button\n",
    "    }\"\"\"\n",
    "\n",
    "ChildExample(child=pn.panel(\"A **Markdown** pane!\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d25ec6a5-5d6f-4def-a3f2-aff9fa0a01b3",
   "metadata": {},
   "source": [
    "The `class_` argument also supports a tuple of types:\n",
    "\n",
    "```python\n",
    "    child = Child(class_=(pn.pane.Markdown, pn.pane.HTML))\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc15da31-0841-4166-8e70-bfa9ea97576a",
   "metadata": {},
   "source": [
    "## Displaying a List of Children\n",
    "\n",
    "You can also display a `List` of `Viewable` objects using the `Children` parameter type:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "71f95d2b-e3fd-49de-890c-901cc0a7693e",
   "metadata": {},
   "outputs": [],
   "source": [
    "import panel as pn\n",
    "\n",
    "from panel.custom import Children, JSComponent\n",
    "\n",
    "\n",
    "class ChildrenExample(JSComponent):\n",
    "\n",
    "    objects = Children()\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const div = document.createElement('div')\n",
    "      div.append(...model.get_child(\"objects\"))\n",
    "      return div\n",
    "    }\"\"\"\n",
    "\n",
    "\n",
    "ChildrenExample(\n",
    "    objects=[pn.panel(\"A **Markdown** pane!\"), pn.widgets.Button(name=\"Click me!\"), {\"text\": \"I'm shown as a JSON Pane\"}]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f2fbec0-5aba-41de-9ccc-b8cf9df6034f",
   "metadata": {},
   "source": [
    ":::note\n",
    "You can change the `item_type` to a specific subtype of `Viewable` or a tuple of\n",
    "`Viewable` subtypes.\n",
    ":::\n",
    "\n",
    "### Render Policy\n",
    "\n",
    "By default the `render_policy='children'`, which means that when we update the children the component is re-rendered (i.e. the contents are cleared and `render` is called again). To manually handle child updates you can set `_render_policy='manual' and define a handler that will add the updated children to the DOM:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fcd9f351-b684-4243-9538-d4ba6147caab",
   "metadata": {},
   "outputs": [],
   "source": [
    "class ManualExample(JSComponent):\n",
    "\n",
    "    objects = Children()\n",
    "\n",
    "    _esm = \"\"\"\n",
    "    export function render({ model }) {\n",
    "      const div = document.createElement('div')\n",
    "      const render_objects = () => div.replaceChildren(...model.get_child('objects'))\n",
    "      model.on('objects', render_objects)\n",
    "      render_objects()\n",
    "      return div\n",
    "    }\"\"\"\n",
    "\n",
    "    _render_policy = 'manual'\n",
    "\n",
    "manual = ManualExample(\n",
    "    objects=[pn.panel(\"A **Markdown** pane!\"), pn.widgets.Button(name=\"Click me!\")]\n",
    ")\n",
    "\n",
    "manual"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d00614d5-b3d8-4770-be4c-c116763cc9b0",
   "metadata": {},
   "source": [
    "Now when we update the `objects` parameter, containing our children, the `render_objects` handler is called instead of re-creating the entire component from scratch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a226ece3-643f-4e0a-aa88-cc78750f444a",
   "metadata": {},
   "outputs": [],
   "source": [
    "manual.objects = [pn.panel(\"A different **Markdown** pane!\"), pn.widgets.Button(name=\"Click me!\")]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d56af9f3-39bd-4c68-bb8e-f889c534326c",
   "metadata": {},
   "source": [
    "## References\n",
    "\n",
    "### Tutorials\n",
    "\n",
    "- [Build Custom Components](../../how_to/custom_components/esm/custom_layout.md)\n",
    "\n",
    "### How-To Guides\n",
    "\n",
    "- [Convert `AnyWidget` widgets](../../how_to/migrate/anywidget/index.md)\n",
    "\n",
    "### Reference Guides\n",
    "\n",
    "- [`AnyWidgetComponent`](AnyWidgetComponent.ipynb)\n",
    "- [`JSComponent`](JSComponent.ipynb)\n",
    "- [`ReactComponent`](ReactComponent.ipynb)"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python",
   "pygments_lexer": "ipython3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
